<html>
<!-- This page can create whatever iframe structure you want, across whatever
sites you want. This is useful for testing site isolation.

Example usage in a browsertest, explained:

  GURL url = embedded_test_server()->
      GetURL("a.com", "/cross_site_iframe_factory.html?a(b(c,d))");

When you navigate to the above URL, the outer document (on a.com) will create a
single iframe:

  <iframe id="child-0" src="http://b.com:1234/cross_site_iframe_factory.html?b(c(),d())">

Inside of which, then, are created the two leaf iframes:

  <iframe id="child-0" src="http://c.com:1234/cross_site_iframe_factory.html?c()">
  <iframe id="child-1" src="http://d.com:1234/cross_site_iframe_factory.html?d()">

Add iframe options by enclosing them in '{' and '}' characters after the
hostname (multiple options can be separated with commas):

  cross_site_iframe_factory.html?a(b{allowfullscreen}(),c{sandbox-allow-scripts}(d))

Will create two iframes:

  <iframe id="child-0" src="http://a.com:1234/cross_site_iframe_factory.html?b()" allowfullscreen>
  <iframe id="child-1" src="http://c.com:1234/cross_site_iframe_factory.html?c{sandbox-allow-scripts}(d())" sandbox="allow-scripts">

To specify the site for each iframe, you can use a simple identifier like "a"
or "b", and ".com" will be automatically appended. You can also specify a port
number to use for each site by using a label like "a:12345". If no port number
is given, the port will default to the port number of the top level frame.
You can also specify an arbitrary full URL for any bottommost leaf frame.

These options are supported:

  allowfullscreen
  allowpaymentrequest
  allow-{featurename} - Adds {featurename} to the "allow" attribute
  sandbox-{flagname} - Adds {flagname} to the "sandbox" attribute

To make this page work, your browsertest needs a MockHostResolver, like:

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(embedded_test_server()->Start());
  }

You can play around with the arguments by loading this page via file://, but
you probably won't get the same process behavior as if you loaded via http. -->
<head>
<title>Cross-site iframe factory</title>
<style>
body {
  font-family: "DejaVu Sans", Sans-Serif;
  text-align: center;
}
iframe {
  border-radius: 7px;
  border-style: solid;
  vertical-align: top;
  margin: 2px;
  box-shadow: 2px 2px 2px #888888;
}
</style>
</head>
<body>
<h2 id='siteNameHeading'></h2>


<script src='tree_parser_util.js'></script>
<script type='text/javascript'>

/**
 * Determines a random pastel-ish color from the first character of a string.
 */
function pastelColorForFirstCharacter(seedString, lightness) {
  // Map the first character to an index. This could be negative, we don't
  // really care.
  var index = seedString.charCodeAt(0) - 'a'.charCodeAt(0);

  // If the first character is 'a', this will the the starting color.
  var hueOfA = 200;  // Spoiler alert: it's blue.

  // Color palette generation articles suggest that spinning the hue wheel by
  // the golden ratio yields a magically nice color distribution. Something
  // about sunflower seeds. I am skeptical of the rigor of that claim (probably
  // any irrational number at a slight offset from 2/3 would do) but it does
  // look pretty.
  var phi = 2 / (1 + Math.pow(5, .5));
  var hue = Math.round((360 * index * phi + hueOfA) % 360);
  return 'hsl(' + hue + ', 60%, ' + Math.round(100 * lightness) + '%)';
}

function backgroundColorForSite(site) {
  // Light pastel.
  return pastelColorForFirstCharacter(site, .75);
}

function borderColorForSite(site) {
  // Darker color in the same hue has the background.
  return pastelColorForFirstCharacter(site, .32);
}

function isUrl(siteString) {
  try {
    var url = new URL(siteString);
  } catch (e) {
    if (e instanceof TypeError)
      return false;
  }
  return true;
}

/**
 * Extract the specified port number, if any. Returns empty string if not
 * specified.
 */
function sitePortNumber(siteString) {
  let index = siteString.indexOf(':');
  if (index == -1)
    return ""
  return siteString.substring(index + 1);
}

/**
 * Adds ".com" to an argument if it doesn't already have a top level domain.
 * This cuts down on noise in the query string, letting you use single-letter
 * names. Adds the specified port number, if any, or the default port otherwise.
 */
function canonicalizeSiteAndPort(siteString, defaultPort) {
  var portNumber = sitePortNumber(siteString) || defaultPort;
  var hostName = siteString.split(':')[0];
  if (hostName !== "localhost" && hostName.indexOf('.') == -1)
    hostName = hostName + '.com';
  return hostName + (portNumber ? ':' + portNumber : "");
}

/**
 * Parses the list of iframe options and applies them to the provided element.
 */
function applyIFrameOptions(element, options) {
  var sandbox = false;
  var sandboxArguments = [];
  var allowFeatures = [];
  for (var option of options) {
    if (option == "allowfullscreen") {
      element.allowFullscreen = true;
    }
    if (option == "allowpaymentrequest") {
      element.allowPaymentRequest = true;
    }
    if (option == "sandbox") {
      sandbox = true;
    }
    if (option.startsWith("sandbox-")) {
      sandbox = true;
      sandboxArguments.push(option.slice(8));
    }
    if (option.startsWith("allow-")) {
      allowFeatures.push(option.slice(6))
    }
  }
  if (sandbox) {
    element.sandbox = sandboxArguments.join(" ");
  }
  if (allowFeatures.length) {
    element.allow = allowFeatures.join(" ");
  }
}

/**
 * Determines whether or not text should be rendered by checking whether
 * "no-text-render" is among the list of attributes.
 */
function shouldRenderText(attributes) {
  for (var attribute of attributes) {
    if (attribute == "no-text-render") {
      return false;
    }
  }
  return true;
}

/**
 * Determines whether or not the frame should be positioned outside of
 * its parent frame by checking whether "out-of-view" is among the list
 * of attributes.
 */
function renderOutsideParentView(attributes) {
  for (var attribute of attributes) {
    if (attribute == "out-of-view") {
      return true;
    }
  }
  return false;
}

/**
 * Simple recursive layout heuristic, since frames can't size themselves.
 * This scribbles .layoutX and .layoutY properties into |tree|.
 */
function layout(tree) {
  // Step 1: layout children.
  var numFrames = tree.children.length;
  for (var i = 0; i < numFrames; i++) {
    layout(tree.children[i]);
  }

  // Step 2: find largest child.
  var largestChildX = 0;
  var largestChildY = 0;
  for (var i = 0; i < numFrames; i++) {
    largestChildX = Math.max(largestChildX, tree.children[i].layoutX);
    largestChildY = Math.max(largestChildY, tree.children[i].layoutY);
  }

  // Step 3: Tweakable control parameters.
  var minX = 110;  // Should be wide enough to fit a decent sized domain.
  var minY = 110;  // Could be less, but squares look nice.
  var extraYPerLevel = 50;  // Needs to be tall enough to fit a line of text.
  var extraXPerLevel = 50;  // Could be less, but squares look nice.

  // Account for padding around each <iframe>.
  largestChildX += 30;
  largestChildY += 30;

  // Step 4: Assume a gridSizeX-by-gridSizeY layout that's big enough to fit if
  // all children were the size of the largest one.
  var gridSizeX = Math.ceil(Math.sqrt(numFrames));
  var gridSizeY = Math.round(Math.sqrt(numFrames));
  tree.layoutX = Math.max(gridSizeX * largestChildX + extraXPerLevel, minX);
  tree.layoutY = Math.max(gridSizeY * largestChildY + extraYPerLevel, minY);
}

function main() {
  var goCrossSite = !window.location.protocol.startsWith('file');
  var queryString = decodeURIComponent(window.location.search.substring(1));
  var frameTree = TreeParserUtil.parse(queryString);
  var currentSite = isUrl(frameTree.value)
                      ? frameTree.value
                      : canonicalizeSiteAndPort(frameTree.value, "");

  // Render text unless the attribute "no-text-render" is specified, in which
  // case load some bytes anyway in order to trigger histogram recording for
  // ad metrics tests.
  if (shouldRenderText(frameTree.attributes)) {
      document.getElementById('siteNameHeading').appendChild(
          document.createTextNode(currentSite));
  }

  // Apply style to the current document.
  document.body.style.backgroundColor = backgroundColorForSite(currentSite);

  // Determine how big the children should be (using a very rough heuristic).
  layout(frameTree);

  for (var i = 0; i < frameTree.children.length; i++) {
    // Compute the URL for this iframe.
    let url = frameTree.children[i].value;
    let siteAndPort = url;
    if (!isUrl(url)) {
      siteAndPort = canonicalizeSiteAndPort(url, window.location.port);
      const subtreeString = TreeParserUtil.flatten(frameTree.children[i]);
      url = '';
      url += window.location.protocol + '//';              // scheme (preserved)
      url += goCrossSite ? siteAndPort : window.location.host;    // host and port
      url += window.location.pathname;                     // path (preserved)
      url += '?' + encodeURIComponent(subtreeString);      // query
    }

    // Construct the iframe.
    var iframe = document.createElement('iframe');
    iframe.src = url;
    iframe.id = "child-" + i;
    iframe.style.borderColor = borderColorForSite(siteAndPort);
    iframe.width = frameTree.children[i].layoutX;
    iframe.height = frameTree.children[i].layoutY;

    // If needed, position the iframe.
    if (renderOutsideParentView(frameTree.children[i].attributes)) {
        iframe.style.position = "fixed";
        iframe.style.top = "150%";
        iframe.style.left = "150%";
    }

    // Apply any additional attributes.
    applyIFrameOptions(iframe, frameTree.children[i].attributes);

    // Add the iframe to the document.
    document.body.appendChild(iframe);
  }
}

main();
</script>
</body></html>
